// Copyright (c) 2022 Presto Labs Pte. Ltd.
// Author: qiang

#include "coin2/exchange/crypto_futures/api_order/native_private_client.h"

#include <list>
#include <utility>

#include <fmt/format.h>

#include "coin2/exchange/base/api_util/url_params_builder.h"
#include "coin2/exchange/base/order/order.h"
#include "coin2/exchange/crypto_common/api_util/auth_key.h"
#include "coin2/exchange/crypto_futures/symbology/product.h"
#include "coin2/exchange/order/crypto.pb.h"

#define CHECK_API_LIMIT(action_type, query_type)                                               \
  if (local_rate_tracker_ &&                                                                   \
      (!local_rate_tracker_->HasEnoughRateLimit(ts, action_type, query_type))) {               \
    LOG_EVERY_N(WARNING, 50) << "Hit rate limit or api temporary disabled until " << __func__; \
    api_block_info_[__func__]++;                                                               \
    return {};                                                                                 \
  }

namespace {
using ActionType = coin2::exchange::crypto_common::api_util::CryptoRateTracker::ActionType;
using coin2::exchange::crypto_common::api_util::CryptoRateTracker;
using QueryType = coin2::exchange::crypto_common::api_util::QueryType;
}  // namespace

CryptoFuturesNativePrivateClient::CryptoFuturesNativePrivateClient(
    const MarketExchangeApi& mea,
    const NativeClientConfig& config) {
  mea_ = mea;
  NativeClientConfig cfg;
  cfg.set_rest_host("deriv-api.crypto.com");
  cfg.set_rest_host_port(443);
  cfg.MergeFrom(config);
  LOG(INFO) << "[CryptoFuturesNativePrivateClient] config: " << cfg.DebugString();

  auths_.Construct<CryptoRestAuth, NativeClientConfig>(cfg);
  rate_tracker_ = std::make_unique<CryptoRateTracker>(mea_, config);

  local_rate_tracker_ = static_cast<CryptoRateTracker*>(rate_tracker_.get());
  start_time_ = GetCurrentTimestamp();
}

auto CryptoFuturesNativePrivateClient::QueryAccountBalance() -> HttpContextPtr {
  UrlParamsBuilder builder;
  auto ts = GetCurrentTimestamp();
  auto context = HttpContextPtr(
      GetAuth()->CreateRequestByPostWithSignature(path_query_account_balance_, &builder));

  if (!is_first_query_account_balance_) {
    CHECK_API_LIMIT(ActionType::QUERY, QueryType::ACCOUNT);
  }
  is_first_query_account_balance_ = false;

  if (local_rate_tracker_) {
    local_rate_tracker_->Push(1, ts, ActionType::QUERY, QueryType::ACCOUNT);
  }

  api_req_info_[__func__]++;
  context->extra_info = DebugString();

  context->query_type = OrderSubTopicId::REST_ACCOUNT_BALANCE;
  return context;
}

auto CryptoFuturesNativePrivateClient::QueryAccountPostion() -> HttpContextPtr {
  UrlParamsBuilder builder;
  auto ts = GetCurrentTimestamp();
  CHECK_API_LIMIT(ActionType::QUERY, QueryType::POSITION);
  auto context = HttpContextPtr(
      GetAuth()->CreateRequestByPostWithSignature(path_query_account_postion_, &builder));
  context->query_type = OrderSubTopicId::REST_ACCOUNT_POSITION;

  if (local_rate_tracker_) {
    local_rate_tracker_->Push(1, ts, ActionType::QUERY, QueryType::POSITION);
  }
  api_req_info_[__func__]++;
  context->extra_info = DebugString();

  return context;
}

auto CryptoFuturesNativePrivateClient::QueryOpenOrders() -> HttpContextPtr {
  UrlParamsBuilder builder;
  auto ts = GetCurrentTimestamp();
  CHECK_API_LIMIT(ActionType::QUERY, QueryType::OPEN_ORDER);
  auto context = HttpContextPtr(
      GetAuth()->CreateRequestByPostWithSignature(path_query_open_orders_, &builder));
  context->query_type = OrderSubTopicId::REST_OPEN_ORDERS;

  if (local_rate_tracker_) {
    local_rate_tracker_->Push(1, ts, ActionType::QUERY, QueryType::OPEN_ORDER);
  }

  api_req_info_[__func__]++;
  context->extra_info = DebugString();
  return context;
}

auto CryptoFuturesNativePrivateClient::QueryHistOrders(const RestParams& params) -> HttpContextPtr {
  UrlParamsBuilder builder;
  auto ts = GetCurrentTimestamp();
  CHECK_API_LIMIT(ActionType::QUERY, QueryType::HIST_ORDER);
  if (params.has_start_time() && params.has_end_time()) {
    builder.PutPost("start_time", params.start_time());
    builder.PutPost("end_time", params.end_time());
  }

  auto context = HttpContextPtr(
      GetAuth()->CreateRequestByPostWithSignature(path_query_hist_orders_, &builder));
  if (local_rate_tracker_) {
    local_rate_tracker_->Push(1, ts, ActionType::QUERY, QueryType::HIST_ORDER);
  }

  api_req_info_[__func__]++;
  context->extra_info = DebugString();
  context->query_type = OrderSubTopicId::REST_DONE_ORDERS;
  return context;
}

auto CryptoFuturesNativePrivateClient::QueryTrades(const RestParams& params) -> HttpContextPtr {
  UrlParamsBuilder builder;
  auto ts = GetCurrentTimestamp();
  CHECK_API_LIMIT(ActionType::QUERY, QueryType::FILL_ORDER);
  if (params.has_start_time() && params.has_end_time()) {
    builder.PutPost("start_time", params.start_time());
    builder.PutPost("end_time", params.end_time());
  }

  auto context =
      HttpContextPtr(GetAuth()->CreateRequestByPostWithSignature(path_query_trades_, &builder));
  if (local_rate_tracker_) {
    local_rate_tracker_->Push(1, ts, ActionType::QUERY, QueryType::FILL_ORDER);
  }

  api_req_info_[__func__]++;
  context->extra_info = DebugString();
  context->query_type = OrderSubTopicId::REST_FILL_ORDERS;
  return context;
}

using RestParams = coin2::exchange::order::CryptoFuturesRestParams;

auto CryptoFuturesNativePrivateClient::PlaceOrder(const RestParams& params) -> HttpContextPtr {
  UrlParamsBuilder builder;
  auto ts = GetCurrentTimestamp();
  CHECK_API_LIMIT(ActionType::PLACE, QueryType::PLACE_ORDER);
  builder.PutPost("instrument_name", params.instrument_name());
  builder.PutPost("side", params.side());
  builder.PutPost("quantity", params.quantity());
  builder.PutPost("price", params.price());
  builder.PutPost("type", params.type());
  builder.PutPost("client_oid", params.client_oid());
  if (params.has_time_in_force()) {
    builder.PutPost("time_in_force", params.time_in_force());
  }
  if (params.has_exec_inst()) {
    std::list<std::string> arr;
    arr.push_back(params.exec_inst());
    builder.PutPost<true>("exec_inst", arr);
  }
  auto context =
      HttpContextPtr(GetAuth()->CreateRequestByPostWithSignature(path_place_order_, &builder));
  CHECK_API_LIMIT(ActionType::PLACE, QueryType::PLACE_ORDER);
  if (local_rate_tracker_) {
    local_rate_tracker_->Push(1, ts, ActionType::PLACE, QueryType::PLACE_ORDER);
  }
  api_req_info_[__func__]++;

  context->extra_info = DebugString();
  context->query_type = OrderSubTopicId::SUBMIT_RESPONSE;
  return context;
}

auto CryptoFuturesNativePrivateClient::CancelOrder(const RestParams& params) -> HttpContextPtr {
  UrlParamsBuilder builder;
  auto ts = GetCurrentTimestamp();
  CHECK_API_LIMIT(ActionType::CANCEL, QueryType::CANCEL_ORDER);
  builder.PutPost("order_id", params.order_id());
  auto context =
      HttpContextPtr(GetAuth()->CreateRequestByPostWithSignature(path_cancel_order_, &builder));

  if (local_rate_tracker_) {
    local_rate_tracker_->Push(1, ts, ActionType::CANCEL, QueryType::CANCEL_ORDER);
  }
  api_req_info_[__func__]++;

  context->extra_info = DebugString();
  context->query_type = OrderSubTopicId::CANCEL_RESPONSE;
  return context;
}

auto CryptoFuturesNativePrivateClient::CancelAllOrder(const RestParams& params) -> HttpContextPtr {
  return {};
}

auto CryptoFuturesNativePrivateClient::PlaceOrderNorm(const OrderExecution& order_exe)
    -> HttpContextPtr {
  const auto& order_spec = *order_exe.order_spec;
  const auto proc_order_id = order_exe.proc_order_id;
  auto params = GetRestOrderParams(order_spec, proc_order_id);
  return PlaceOrder(params);
}

auto CryptoFuturesNativePrivateClient::CancelOrderNorm(const OrderExecution& order_exe)
    -> HttpContextPtr {
  auto params = GetRestCancelParams(*order_exe.oc);
  return CancelOrder(params);
}

std::string CryptoFuturesNativePrivateClient::DebugString() {
  double delta_t = (GetCurrentTimestamp() - start_time_) / 1.0e9;

  std::stringstream ss;
  ss << "CryptoFuturesNativePrivateClient::DebugString\n";
  ss << "api info\n";
  for (auto& [name, value] : api_req_info_) {
    int32_t blocked = 0;
    if (api_block_info_.count(name) > 0) {
      blocked = api_block_info_.at(name);
    }
    int32_t total_req = value + blocked;
    ss << name << " (total:" << total_req << ", succeeded: " << value << ", blocked: " << blocked
       << ", req_rate: " << value / delta_t << ", block_rate: " << blocked / delta_t << ")\n";
  }
  return ss.str();
}

auto CryptoFuturesNativePrivateClient::GetRestOrderParams(
    const OrderSpec& order_spec,
    int64_t proc_order_id) -> RestParams {
  RestParams params;

  using coin2::exchange::crypto_futures::symbology::CryptoFuturesProduct;
  auto product = CryptoFuturesProduct::FromStr(order_spec.product, order_spec.order_created_time);
  auto product_holder = GetProductHolder(product, order_spec.order_created_time);
  auto formatter = product_holder->product_info().order_formatter();
  params.set_client_oid(fmt::format("{}", proc_order_id));
  params.set_instrument_name(order_spec.product);
  if (order_spec.order_side == OrderSide::BUY_ORDER) {
    params.set_side("BUY");
  } else if (order_spec.order_side == OrderSide::SELL_ORDER) {
    params.set_side("SELL");
  } else {
    NOTREACHED() << "Unknown order side: " << static_cast<int>(order_spec.order_side);
  }
  params.set_quantity(formatter.FormatQty(order_spec.qty));
  if (order_spec.order_type == OrderType::LIMIT_ORDER) {
    params.set_type("LIMIT");
    params.set_price(formatter.FormatPrice(order_spec.price));
    if (order_spec.duration == coin::proto::OrderDuration::FOK_ORDER) {
      params.set_time_in_force("FILL_OR_KILL");
    } else if (order_spec.duration == coin::proto::OrderDuration::IOC_ORDER) {
      params.set_time_in_force("IMMEDIATE_OR_CANCEL");
    } else {
      params.set_time_in_force("GOOD_TILL_CANCEL");
    }
  } else if (order_spec.order_type == OrderType::MARKET_ORDER) {
    params.set_type("MARKET");
  } else {
    NOTREACHED() << "unsupported order type: " << static_cast<int>(order_spec.order_type);
  }
  if (order_spec.post_only) {
    params.set_exec_inst("POST_ONLY");
  }

  return params;
}

auto CryptoFuturesNativePrivateClient::GetRestCancelParams(const IOrderContext& order_context)
    -> RestParams {
  RestParams params;
  params.set_instrument_name(order_context.native_product());
  params.set_order_id(order_context.exchange_order_id());

  return params;
}

std::shared_ptr<AsioHttpContext> CryptoFuturesNativePrivateClient::SubmitOrderExecution(
    const OrderExecutionProto& order) {
  switch (order.type()) {
    case OrderExecutionProto::SUBMIT_ORDER: {
      auto order_spec = OrderSpec::FromProto(order.order_spec());
      auto params = GetRestOrderParams(*order_spec, order.proc_order_id());
      return PlaceOrder(params);
    }
    case OrderExecutionProto::CANCEL_ORDER: {
      auto order_spec = OrderSpec::FromProto(order.order_spec());
      auto params = GetRestOrderParams(*order_spec, order.proc_order_id());
      return CancelOrder(params);
    }
    default: {
      return nullptr;
    }
  }
}

std::shared_ptr<AsioHttpContext> CryptoFuturesNativePrivateClient::SubmitRestQuery(
    const RestQueryProto& query) {
  RestParams params;
  switch (query.type()) {
    case RestQueryProto::QUERY_ACCOUNT_BALANCE: {
      return QueryAccountBalance();
    }
    case RestQueryProto::QUERY_OPEN_ORDERS: {
      return QueryOpenOrders();
    }
    default: {
      return nullptr;
    }
  }
}
